<?php
// +----------------------------------------------------------------------
// | Bwsaas
// +----------------------------------------------------------------------
// | Copyright (c) 2015~2020 http://www.buwangyun.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Gitee ( https://gitee.com/buwangyun/bwsaas )
// +----------------------------------------------------------------------
// | Author: buwangyun <hnlg666@163.com>
// +----------------------------------------------------------------------
// | Date: 2020-9-28 10:55:00
// +----------------------------------------------------------------------

namespace buwang\service;


use buwang\util\File;
use buwang\util\Sql;
use think\Exception;
use think\facade\Db;
use buwang\util\Http;
use buwang\exception\MiniappException;
use think\facade\Log;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Alchemy\Zippy\Zippy;
use app\common\model\Miniapp;
use app\common\model\MiniappModule;
use app\manage\model\AuthGroup;
use app\manage\model\AuthGroupAccess;
use app\manage\model\AuthGroupNode;
use app\manage\model\AuthNode;
use app\common\model\MemberMiniapp;
use app\manage\model\Member;
use app\manage\model\Config;
use app\manage\model\ConfigTab;
use app\manage\model\ConfigGroup;
use app\manage\model\ConfigGroupData;
use buwang\traits\ErrorTrait;


/**
 * 应用更新服务类
 * Class PluginService
 * @package buwang\service
 */
class MiniappUpdateService
{
    use ErrorTrait;

    protected $version = "1.0.0"; //初始化版本
    protected $domain = ""; //云接口域名
    protected $download_url = ""; //应用更新下载路由接口
    protected $info_url = ""; //获取最新应用信息路由
    protected $local_cofig = []; //原配置安装文件
    protected $local_group_data = []; //原组合数据安装文件
    protected $temp_path = '';//临时文件目录
    protected $local_file = '';//本地包路径
    protected $ds = '';//路径分割
    protected $root_path = '';//根路径

    public function __construct($data = [])
    {
        //初始化配置信息
        $miniapp_config = config('miniapp'); //得到应用云配置
        $miniapp_config = array_merge($miniapp_config, $data); //应用配置
        $this->domain = $miniapp_config['domain'];
        $this->download_url = $miniapp_config['download'] ?? '';
        $this->info_url = $this->domain . $miniapp_config['info'];
        $this->ds = DS;
        $this->root_path = ROOT_PATH;
        $this->temp_path = "{$this->root_path}{$this->ds}runtime{$this->ds}";
    }

    /**
     * 检查并获取更新信息
     */
    public function info($dir, $check_local = true)
    {
        //得到远程最新版本信息
        $remote_version = $this->getRemoteVersion($dir);
        //远程库是否有可供下载的文件
        if (!$remote_version['data']['data'] || !$remote_version['data']['data']['have_app_file']) throw new MiniappException('云端暂无该应用的更新库，无需更新');
        //得到当前应用配置
        $local_version = MiniappService::getVersion($dir);//应用本地版本
        $this->version = $local_version['version'];
        if (empty($local_version)) throw new MiniappException('未找到应用配置');
        //比较版本差异
        if ($this->compareVersion($this->version, $remote_version['data']['data']['version']) >= 0) throw new MiniappException('当前已是最新版本，无需更新。');
        //拼装返回数据
        $remote_version['data']['data']['local_package'] = false;
        //检测是否有本地包
        if ($check_local) $remote_version['data']['data']['local_package'] = $this->checkLocalPackage($dir);
        return $remote_version['data']['data'];
    }


    public function cover($dir)
    {
        //查询应用
        $miniapp = Miniapp::where('dir', $dir)->find();
        if (!$miniapp) throw new MiniappException('请先进行应用的安装');
        //得到文件覆盖前本地原项目配置和组合配置文件
        $this->local_cofig = MiniappService::getAppConfig($dir);//配置
        $this->local_group_data = MiniappService::getAppData($dir);//组合数据
        //TODO: 2021/5/6 变动，如果存在本地包，优先本地包
        //①检测本地包是否存在
        $remote_version_info = $this->checkLocalPackage($dir);
        if ($remote_version_info) {
            //②存在本地包获取本地包
            $tempFile = $this->local_file;
        } else {
            //③不存在本地包获取远程包
            //得到最新远程下载信息
            $remote_version_info = $this->info($dir, false);
            //获取远程文件
            $version = $remote_version_info['version'];
            $this->download_url = $remote_version_info['download_url'];
            $tempDirectory = "{$this->temp_path}app_update_depository{$this->ds}{$dir}{$this->ds}{$version}";
            $tempFile = "{$tempDirectory}{$this->ds}src.zip";//本地临时文件生成地址
            $this->download($this->download_url, $tempFile);
        }
        //将最新项目替换掉当前项目
        $zippy = Zippy::load();
        $archive = $zippy->open($tempFile);
        $cover_path = "{$this->root_path}{$this->ds}app{$this->ds}";
        $archive->extract($cover_path); //执行项目文件覆盖
        unset($archive);//释放占用内存
        $this->coverStaticResource($dir);//执行覆静态资源盖
        //执行应用数据增量更新
        $this->updateDatabase($dir, true);
        //调用应用钩子事件
        event('MiniappUpdate', [$dir, $remote_version_info]);
        return true;
    }

    /**
     * TODO 20230629执行应用数据增量更新
     */
    public function updateDatabase($dir, $trans = false)
    {
        if ($trans) {
            Db::startTrans();
        }
        try {
            //执行更新sql（如果存在update.sql的话）
//            MiniappService::runSQL($dir, 'install', 'update');弃置
            $this->exeUpdateSql($dir);//执行更新sql
            //执行节点更新(如果存在节点)
            //定义一个常量，菜单使用
            define('MINIAPP_DIR', $dir);
            $this->updateGroupNode($dir);
            // 执行配置和组合数据更新(分类节点修复)
            $this->updateConfigAndGroupData($dir);
            if ($trans) {
                Db::commit();
            }
        } catch (\Exception $e) {
            if ($trans) {
                Db::rollback();
            }
            throw new MiniappException($e->getMessage());
        }
    }


    /**得到应用云端最新版本信息
     * @param $dir
     * @return bool|string
     */
    public function getRemoteVersion($dir)
    {
        try {
            $res = Http::getBwRequest($this->info_url, ['dir' => $dir]);
        } catch (\Exception $e) {
            throw new MiniappException($e->getMessage());
        }
        return $res;
    }

    /**版本比较 1 本地版本高于远程版本，0 本地版本 等于远程版本，-1 本地版本小于远程版本
     * @param $local_version
     * @param $remote_version
     * @return bool
     */
    public function compareVersion($local_version, $remote_version)
    {
        //正则成统一格式
        $local_version = $this->versionFormat($local_version);
        $remote_version = $this->versionFormat($remote_version);
        //比较大小
        return bccomp($local_version, $remote_version);
    }


    /**
     * 版本号格式化
     */
    public function versionFormat($versionString)
    {
        $versionString = preg_replace("([a-zA-Z:=\"\']+)", "", strtolower($versionString));
        if (!$versionString) throw new MiniappException('版本号格式异常，请检查配置');
        $normal_num = 2;//点个数
        $point_num = substr_count($versionString, "."); //点数量
        if ($point_num > $normal_num) throw new MiniappException('版本号格式异常，版本位数过多');
        //补满小数点位数
        for ($i = 1; $i <= ($normal_num - $point_num); $i++) {
            $versionString .= '.00';
        }
        $versionString = explode('.', $versionString);//字符串分割数组
        foreach ($versionString as &$string) {
            $len = strlen($string);
            if (strlen($string) > 2) throw new MiniappException('版本号格式异常，请检查配置');

            $string = str_pad($string, 3 - $len, '0', STR_PAD_LEFT);
        }
        return implode("", $versionString);
    }

    public function download($url, $file)
    {
        if (!is_dir(dirname($file))) {
            if (!make_dir(dirname($file))) {
                Log::error(dirname($file) . '无法创建目录，请检查文件写入权限。');
                throw new MiniappException('无法创建目录，请检查文件写入权限。');
            }
        }
        $fp = fopen($file, 'w+');
        if ($fp === false) {
            Log::error('无法保存文件，请检查文件写入权限。');
            throw new MiniappException('无法保存文件，请检查文件写入权限。');
        }
        try {
            $client = new Client([
                'verify' => false,
                'stream' => true,
            ]);
            $response = $client->get($url);
            $body = $response->getBody();
            while (!$body->eof()) {
                fwrite($fp, $body->read(1024));
            }
            fclose($fp);
        } catch (\Exception $e) {
            throw new MiniappException('云端暂无更新文件，请联系官方客服或使用【本地包】进行更新！');
        }
        return $file;
    }


    /**
     * 得到本地应用sql文件内容（字符串）
     */
    public function getLocalSql(string $name = '', string $dir = 'install', string $sqlName = 'install')
    {
        $sql_file = base_path($name) . DS . "{$dir}" . DS . "{$sqlName}.sql";
        if (!file_exists($sql_file)) return '';

        $table_prefix = config('database.connections.mysql.prefix');
        $sql_statement = Sql::getSqlFromFile($sql_file, false, [], $table_prefix);
//        if (!empty($sql_statement)) {
//            //修改表前缀
//            $table_prefix = config('database.connections.mysql.prefix');
//            if (!empty($table_prefix)) {
//                $sql_statement = str_replace('bw_', $table_prefix, $sql_statement);
//            }
//        }
        return $sql_statement;
    }

    /**得到本地安装菜单文件
     * @param $name
     * @return array|mixed
     */
    public function getLocalGroup($name)
    {
        $path = base_path($name) . DS . "install" . DS . "menu.php";
        if (!file_exists($path)) return [];
        $data = include $path; //得到角色组和菜单节点树
        if (empty($data)) return [];
        return $data;
    }


    /**更新菜单节点数据
     * @param string $name
     */
    public function updateGroupNode(string $name)
    {
        //得到安装节点数据
        $data = $this->getLocalGroup($name);
        if (!$data) return true;
        //获取所有应用
        $miniapps = array_flip(Miniapp::column('dir', 'id')); //以目录名为key 应用id为value的数组
        //待插入应用功能
        $modules = [];
        foreach ($data as $item) {
            //遍历最外层  = 角色组
            //TODO 参数验证
            //组装角色组数据
            //角色记录
            $searchParam = [
                //角色唯一标识,不可重复
                'group_name' => $name . '_' . $item['group_name'], //生成规则：应用目录名 + ‘_’+ 角色组名
                //登陆类型 admin=总后台,member=租户后台
                'scopes' => 'member',  //目前应用属于租户，则角色组只可能是租户后台的，即member
                'type' => 'app',//角色组类型：system=系统，app=应用，plugin=插件
                'app_name' => $name,//应用名称 取目录名
            ];
            $groupParam = $searchParam;
            $groupParam['name'] = $item['name'];
            $groupParam['remark'] = $item['remark'] ?: '';
            //判断角色是否存在（标识，范围，应用名一致的角色组），不存在则作插入操作
            $group = AuthGroup::where($searchParam)->find();
            if (!$group) $group = AuthGroup::create($groupParam); //创建角色组
            if (!$group) throw new Exception("{$item['name']}角色导入失败");
            //应用模块记录 如果应用没有安装进表中则抛错
            if (!isset($miniapps[$name])) throw new Exception("{$name}应用不存在");
            //查询是否有该功能表，如果没有则插入
            //TODO; jyk 2021-7-6 16:14:32 应用更新 去除套餐的维护
//            $module = MiniappModule::where('group_id', $group['id'])->where('miniapp_id', $miniapps[$name])->find();
//            if (!$module) {
//                $modules[] = [
//                    'group_id' => $group['id'], //角色组id
//                    'miniapp_id' => $miniapps[$name], //系统应用id
//                    'name' => $item['name'], //角色名称
//                    'type' => $item['module_type'] ?: '1', //是否是基础功能1:基础功能,2:附加功能
//                    'price' => $item['module_price'] ?: 0, //模块价格
//                    'desc' => $item['remark'] ?: '', //描述
//                ];
//            }
            //查询该角色拥有的全部节点
            $node_ids = AuthGroupNode::where('group_id', $group['id'])->where('type', 'app')->column('node_id');
            $node_ids = implode(",", $node_ids);
            //删除该角色所有节点
            if ($node_ids) AuthNode::whereRaw("id IN({$node_ids})")->where('type', 'app')->where('app_name', $name)->delete();
            //删除该角色所有中间表数据
            AuthGroupNode::where('group_id', $group['id'])->where('type', 'app')->delete();
            //插入当前应用的节点
            //如果角色有节点则插入节点
            if (isset($item['nodes']) && !empty($item['nodes'])) MiniappService::importNodeData($name, $group, $item['nodes']); //目录名 ，角色组，菜单权限节点树
        }
//        if ($modules) {
//            $miniappModule = new MiniappModule();
//            $miniappModule->saveAll($modules);
//        }
        return true;

    }

    /**
     * 更新配置和数据组（废弃，更新不会使用）
     */
    public function updateConfigAndGroupDataAbandonment($dir, $trans = false)
    {
        //得到新版安装文件中的sql
        $new_sql = $this->getLocalSql($dir, 'install', 'install');
        //得到新增的配置和数据组(原版本差异比较后新增的配置和组合数据)
        $new_data = $this->getNeedUpdataConfigAndData($dir);
        //正则匹配新增的配置并执行初始化配置插入sql
        $this->runConfigSql($new_sql, $new_data);
        //对当前线上已经购买该应用的用户执行新增配置的插入（针对不会再触发购买事件的租户）
        $this->updataMemberConfigAndGrounpData($dir, $new_data);

    }

    /**
     * 更新配置和数据组修复分类id
     */
    public function updateConfigAndGroupData($dir, $trans = false)
    {
        //跨应用调用model前调用此方法设置应用表前缀
        set_miniapp_database_prefix($dir);
        if ($trans) {
            Db::startTrans();
        }
        try {
            try {
                //得到所有应用配置分类
                $config_tab_ids = ConfigTab::valiWhere('', null, $dir, false)->where('dir', $dir)->where('scopes', 'member')->column('id', 'tab_name');
            } catch (\Exception $e) {
                //TODO  JYK 20210701 修复：如果该应用未使用任何应用配置也没有创建应用配置表不能算报错
                $config_tab_ids = [];
            }
            //遍历配置分类，给租户插入配置数据
            foreach ($config_tab_ids as $tab_name => $tab_id) {
                //修正分类id参数
                Config::valiWhere('', null, $dir, false)->where('tab_name', '=', $tab_name)->where('dir', $dir)->update(['tab_id' => $tab_id]);
            }

            //如果存在组合数据，进行租户插入逻辑
            try {
                $group_ids = ConfigGroup::valiWhere('', null, $dir)->where('dir', $dir)->where('scopes', 'member')->column('id', 'config_name');
            } catch (\Exception $e) {
                //TODO  JYK 20210701 修复：如果该应用未使用任何组合数据也没有创建组合数据表不能算报错
                $group_ids = [];
            }


            //遍历数据组，给租户插入组合数据
            foreach ($group_ids as $config_name => $group_id) {
                //修正数据组id参数
                ConfigGroupData::valiWhere('', null, $dir)->where('config_name', '=', $config_name)->where('dir', $dir)->update(['group_id' => $group_id]);
            }
            if ($trans) {
                Db::commit();
            }

        } catch (\Exception $e) {
            if ($trans) {
                Db::rollback();
            }
            throw new MiniappException("更新配置和组合数据失败：" . $e->getMessage());
        }

        return true;
    }


    /**覆盖静态资源
     * @param $dir
     */
    public function coverStaticResource($dir)
    {
        //1.拷贝静态资源文件 app/demoApp/install/static/ => public/static/demoApp/
        if (file_exists(root_path() . 'app' . DS . $dir . DS . 'install' . DS . 'static' . DS)) {
            //拷贝模板到前台模板目录中去 public/static/demoApp/
            File::copy_dir(root_path() . 'app' . DS . $dir . DS . 'install' . DS . 'static' . DS, root_path() . 'public' . DS . 'static' . DS . strtolower($dir) . DS);
        }

    }


    /**得到需要更新的组合数据的配置
     * @param $dir
     */
    public function getNeedUpdataConfigAndData($dir)
    {
        //得到新增的配置和数据组
        $new_config = MiniappService::getAppConfig($dir);//配置
        $new_group_data = MiniappService::getAppData($dir);//组合数据
        $update_config = $update_group_data = [];
        //得到需要新增的配置
        if ($new_config && isset($new_config['config_name']) && $new_config['config_name']) {
            //遍历寻找差异
            $configs = $new_config['config_name'];
            foreach ($configs as $config_tab => $config) {
                //如果旧系统不存在该配置分类，直接将整个分类配置放入更新中
                if (!$this->local_cofig || !isset($this->local_cofig['config_name']) || !$this->local_cofig['config_name'] || !isset($this->local_cofig['config_name'][$config_tab]) || !$this->local_cofig['config_name'][$config_tab]) {
                    $update_config[$config_tab] = $config;
                } else {
                    //如果存在，则比较该分类下配置的差集
                    $update_config[$config_tab] = array_diff($config, $this->local_cofig['config_name'][$config_tab]);
                    //如果不存在差异则不列入更新
                    if (!$update_config[$config_tab]) unset($update_config[$config_tab]);
                }

            }
        }
        //得到需要更新的数据组
        if ($new_group_data && isset($new_group_data['config_name']) && $new_group_data['config_name']) {
            //原版本不存在组合数据，则直接全部新增
            if (!$this->local_group_data || !isset($this->local_group_data['config_name']) || !$this->local_group_data['config_name']) {
                $update_group_data = $new_group_data['config_name'];
            } else {
                //得到需要新增的数据组
                $update_group_data = array_diff($new_group_data['config_name'], $this->local_group_data['config_name']);
            }
        }
        return compact('update_config', 'update_group_data');
    }


    /**正则匹配并执行对应配置和组合数据的初始化sql（方法废弃）
     * @param $new_sql
     * @param $new_data
     */
    public function runConfigSql($new_sql, $new_data)
    {
        $update_sql = [];
        $configs = $new_data['update_config']; //配置文件
        $group_datas = $new_data['update_group_data']; //数据组
        if ($configs || $group_datas) {
            foreach ($new_sql as $sql) {
                //正则插入语句匹配
                if (preg_match("/^INSERT INTO.*/iUs", $sql)) {
                    //配置
                    if ($configs) {
                        foreach ($configs as $tab => $config) {
                            //配置分类sql插入
                            if (stripos($sql, "`bw_sys_config_tab`") && stripos($sql, "`tab_name` = '{$tab}'")) {
                                $update_sql[] = $sql;
                            }
                            //配置文件sql插入
                            if (stripos($sql, "`bw_sys_config`") && stripos($sql, "`tab_name` = '{$tab}'")) {
                                $update_sql[] = $sql;
                            }
                        }
                    }
                    //数据组
                    if ($group_datas) {
                        foreach ($group_datas as $group_data) {
                            //数据组sql插入
                            if (stripos($sql, "`bw_sys_config_group`") && stripos($sql, "`config_name` = '{$group_data}'")) {
                                $update_sql[] = $sql;
                            }
                            //数据组数据sql插入
                            if (stripos($sql, "`bw_sys_config_group_data`") && stripos($sql, "`config_name` = '{$group_data}'")) {
                                $update_sql[] = $sql;
                            }

                        }
                    }
                }
            }
            //执行sql
            if ($update_sql) {
                //修改表前缀
                $table_prefix = config('database.connections.mysql.prefix');
                if (!empty($table_prefix)) {
                    $update_sql = str_replace('bw_', $table_prefix, $update_sql);
                }

                foreach ($update_sql as $value) {
                    try {
                        Db::execute($value);
                    } catch (\Exception $e) {
                        throw new MiniappException($e->getMessage());
                    }
                }
            }
        }
        return true;
    }

    /**对当前线上已经购买该应用的用户执行新增配置的插入（针对不会再触发购买事件的租户）
     * @param $dir
     * @param $new_data
     */
    public function updataMemberConfigAndGrounpData($dir, $new_data, $trans = false)
    {
        $member_ids = $this->getMemberIdsByApp($dir);
        $config = $new_data['update_config'];//需要插入的配置数据
        $config_data = $new_data['update_group_data'];  //需要插入的组合数据
        $config_insert_list = [];//添加的配置数据
        $group_data_insert_list = [];//添加的组合数据
        $config_tabs = [];
        $group_names = [];
        //增量更新的配置项
        $update_config = [];
        //配置
        if ($config) {  //判断应用是否存在配置数据
            //如果存在配置数据，进行租户插入逻辑
            $config_names = $config;//得到所有的配置组 （二维数组 ， 一维下标是配置组标识 ，二维是配置组下的每一个配置标识）
            //检查配置分类是否有缺少
            $config_tabs = array_keys($config_names); //得到所有配置分类标识数组
            //判断 当前应用 安装配置文件中所记录的配置标识数量，是否与实际存在于数据库中的应用配置组数量一致 ，如果不一致，说明应用sql执行错误，数据库与实际应安装配置分类不一致。
            $config_tab_ids = ConfigTab::where('tab_name', 'in', $config_tabs)->where('dir', 'null')->where('scopes', 'member')->column('id', 'tab_name');
            if (count($config_tabs) != count($config_tab_ids)) throw new MiniappException("与版本更新期望参数不符:在bw_config.php中配置分类数量与实际安装参数不一致");
            //遍历配置分类，给租户插入配置数据
            foreach ($config_tab_ids as $tab_name => $tab_id) {
                //检测该配置分类是否与配置文件中所写的分类一致
                if (!$config_names[$tab_name]) throw new MiniappException("与版本更新期望参数不符:未在bw_config.php中{$tab_name}配置分类下写入配置");
                //得到该分类下 应用安装sql 执行后插入的那些 初始化配置，并比对与配置文件中所写的那些配置是否一致
                $config_list = Config::where('member_id', 0)->where('dir', 'null')->where('scopes', 'member')->where('config_name', 'in', $config_names[$tab_name])->select()->toArray();
                if (count($config_names[$tab_name]) != count($config_list)) throw new MiniappException("与版本更新期望参数不符:在bw_config.php中{$tab_name}配置分类下实际配置数量与实际安装参数不一致");

                $update_config = array_merge($update_config, $config_names[$tab_name]);
                //组装插入数据
                foreach ($config_list as $config_init) {
                    //组装要插入的 应用初始化配置数据（member_id =0） 和 每个租户的配置数据
                    unset($config_init['id']);
                    $config_init['member_id'] = 0; //初始化配置的member_id=0
                    $config_init['tab_id'] = $tab_id;
                    $config_init['dir'] = $dir;
                    //插入初始化数据
                    $config_insert_list[] = $config_init;
                    //给每个租户插入配置
                    foreach ($member_ids as $member_id) {
                        $config_init['member_id'] = $member_id;
                        $config_insert_list[] = $config_init;
                    }
                }
            }
        }

        //组合数据
        if ($config_data) {  //判断应用是否存在组合数据
            //如果存在组合数据，进行租户插入逻辑
            $group_names = $config_data;//得到所有的数据组标识数组 （一维数组 ， value是数据组标识 ）
            //根据数据组标识，得到该应用所有  安装sql 执行后插入的那些 初始化组合数据，并比对与配置文件中所写的那些数据组是否一致
            $group_ids = ConfigGroup::where('config_name', 'in', $group_names)->where('dir', 'null')->where('scopes', 'member')->column('id', 'config_name');
            if (count($group_names) != count($group_ids)) throw new MiniappException("与版本更新期望参数不符:在bw_data.php中组合数据数量与实际安装参数不一致");
            //遍历数据组，给租户插入组合数据
            foreach ($group_ids as $config_name => $group_id) {
                //得到该数据组的全部初始化组合数据（member_id=0的数据）
                $group_data_list = ConfigGroupData::where('member_id', 0)->where('dir', 'null')->where('scopes', 'member')->where('config_name', '=', $config_name)->select()->toArray();
                //组装插入数据
                foreach ($group_data_list as $group_data) {
                    //组装要插入的 应用初始化组合数据（member_id =0） 和 每个租户的组合数据
                    unset($group_data['id']);
                    $group_data['member_id'] = 0; //初始化组合数据的member_id=0
                    $group_data['group_id'] = $group_id;
                    $group_data['dir'] = $dir;
                    //插入初始化组合数据
                    $group_data_insert_list[] = $group_data;
                    //给每个租户插入组合数据
                    foreach ($member_ids as $member_id) {
                        $group_data['member_id'] = $member_id;
                        $group_data_insert_list[] = $group_data;
                    }
                }
            }
        }
        if ($trans) {
            Db::startTrans();
        }

        $res = $res1 = $res2 = $res3 = $res4 = true;
        try {
            //如果应用存在需要插入的配置分类
            if ($config_insert_list) {
                if ($config_tabs) {
                    //执行配置插入前，先删除安装sql插入的初始化配置（如果有），和该应用所有租户配置（如果有）
                    Config::where('tab_name', 'in', $config_tabs)->where('config_name', 'in', $update_config)->where('dir', $dir)->delete();
                    Config::where('tab_name', 'in', $config_tabs)->where('config_name', 'in', $update_config)->where('dir', 'null')->delete();
                }

                //执行配置插入
                $configModel = new Config;
                $res2 = $configModel->saveAll($config_insert_list);
                if ($config_tabs) {
                    //更新配置分类dir标识为当前应用标识
                    ConfigTab::where('tab_name', 'in', $config_tabs)->where('dir', 'null')->update(['dir' => $dir]);
                    Config::where('tab_name', 'in', $config_tabs)->where('dir', 'null')->update(['dir' => $dir]);
                }
            }
            //如果应用存在需要插入的组合数据
            if ($group_data_insert_list) {
                if ($group_names) {
                    //执行组合数据插入前，先删除安装sql插入的初始化组合数据（如果有），和该应用所有租户组合数据（如果有）
                    ConfigGroupData::where('config_name', 'in', $group_names)->where('dir', $dir)->delete();
                    ConfigGroupData::where('config_name', 'in', $group_names)->where('dir', 'null')->delete();
                }

                //执行组合数据插入
                $groupDataModel = new ConfigGroupData;
                $res4 = $groupDataModel->saveAll($group_data_insert_list);
                if ($group_names) {
                    //更新数据组dir标识为当前应用标识
                    ConfigGroup::where('config_name', 'in', $group_names)->where('dir', 'null')->update(['dir' => $dir]);
                    ConfigGroupData::where('config_name', 'in', $group_names)->where('dir', 'null')->update(['dir' => $dir]);
                }
            }

            $res = $res && $res1 && $res2 && $res3 && $res4 && true;

            if ($res) {
                if ($trans) {
                    Db::commit();
                }
            } else {
                if ($trans) {
                    Db::rollback();
                }
            }
        } catch (\Exception $e) {
            if ($trans) {
                Db::rollback();
            }
            throw new MiniappException($e->getMessage());
        }
        if (!$res) throw new MiniappException("更新配置和组合数据失败");

        return $res;

    }


    /**
     * 执行数据库脚本(通过sql语句集字符串)
     * @param string $name 应用目录
     * @param string $dir 数据库脚本所在目录,默认install
     * @param string $sqlName 数据库脚本名称,默认install
     * @return bool
     * @throws Exception
     */
    public function runSqlByStr(string $sql = '', string $dir = '', $have_member = false, $error = false)
    {
        if ($sql) {
            // 分割语句
            $sql_statement = Sql::parseSql($sql, false, []);
            if (!empty($sql_statement)) {
                //修改表前缀
                $table_prefix = config('database.connections.mysql.prefix');
                if (!empty($table_prefix)) {
                    $sql_statement = str_replace('bw_', $table_prefix, $sql_statement); //替换前缀 兼容老版本
                    $sql_statement = str_replace('__BWPREFIX__', $table_prefix, $sql_statement); //替换前缀
                    $sql_statement = str_replace('__BWNAME__', $dir, $sql_statement);  //替换应用标识 __BWNAME__
                }
                foreach ($sql_statement as $value) {
                    try {
                        if ($have_member) {
                            //执行初始化数据
                            $statement = str_replace('__BWMEMBER__', 0, $value);
                            $statement = str_replace('__MEMBER_MINIAPP__', 0, $statement);

                            Db::execute($statement);
                            //得到已经购买的租户
                            $member_ids = $this->getMemberIdsByApp($dir);
                            if ($member_ids) {
                                //给每个租户执行初始化数据
                                foreach ($member_ids as $member_id) {
                                    //得到购买的租户应用id
                                    $memberMiniappId = MemberMiniapp::getMemberMiniappIdByDir($dir, $member_id);
                                    if (!$memberMiniappId) throw new \Exception("更新失败：应用{$dir}对应的租户{$member_id}找不到购买数据");
                                    $member_statement = str_replace('__BWMEMBER__', $member_id, $value);//替换执行
                                    $member_statement = str_replace('__MEMBER_MINIAPP__', $memberMiniappId, $member_statement);
                                    Db::execute($member_statement);
                                }
                            }
                        } else {
                            Db::execute($value);
                        }
                    } catch (\Exception $e) {
                        Log::error("{$dir}应用版本更新执行update.php--语句:[ {$value} ]执行失败，原因：" . $e->getMessage());
                        if ($error) throw new MiniappException($e->getMessage());
                    }
                }
            }
        }
        return true;
    }


    /**得到本地更新文件
     * @param $name
     * @return array|mixed
     */
    public function getUpdateFile($name)
    {
        $path = base_path($name) . DS . "update" . DS . "update.php";
        if (!file_exists($path)) return [];
        $data = include $path; //得到角色组和菜单节点树
        if (empty($data)) return [];
        return $data;
    }


    /**通过应用标识查询当前已购应用到的租户id
     * @param $name
     */
    public function getMemberIdsByApp($dir)
    {
        //查询应用
        $miniapp = Miniapp::where('dir', $dir)->find();
        if (!$miniapp) throw new MiniappException('未找到应用安装记录，请先进行应用的安装');
        //得到购买了应用的租户
        $member_ids = MemberMiniapp::where('miniapp_id', $miniapp['id'])->column('member_id');
        $member_ids_str = implode(",", $member_ids);
        //过滤掉不存在的租户
        if ($member_ids_str) $member_ids = Member::whereRaw("id IN({$member_ids_str})")->column('id');
        return $member_ids;
    }


    /**解释执行更新sql
     * @param $name
     */
    public function exeUpdateSql($name)
    {
        //得到更新文件
        $updateFile = $this->getUpdateFile($name);
        if (!$updateFile) return true; //没有更新文件需要执行
        $commonSql = $extendSql = '';
        //得到可直接执行的sql
        if (isset($updateFile['common'])) $commonSql = $updateFile['common'];
        //得到必须给每个租户执行的sql
        if (isset($updateFile['extend'])) $extendSql = $updateFile['extend'];
        //执行sql
        if ($commonSql) $this->runSqlByStr($commonSql, $name);
        if ($extendSql) $this->runSqlByStr($extendSql, $name, true);
    }

    /** 检测是否存在本地更新包
     * @param $localFile 本地文件地址
     */
    public function checkLocalPackage($dir, $is_bool = false)
    {
        //本地包路径：项目根路径/[ 应用标识 ].zip
        $this->local_file = "{$this->root_path}{$this->ds}public{$this->ds}{$dir}.zip";
        //是否存在本地包文件
        if (!file_exists($this->local_file)) return false;   //不存在返回
        if ($is_bool) return true;
        //得到本地包版本信息
        $remote_version = $this->getLocalPackgeVersion($this->local_file, "{$this->temp_path}app_update_depository{$this->ds}{$dir}{$this->ds}", $dir);
        //本地包完整性检测
        if (!$remote_version) throw new MiniappException('更新失败，本地包缺失版本文件');
        $this->versionCheck($remote_version, $dir);
        //返回
        return $remote_version;
    }

    /**
     * 得到本地包的
     */
    public function getLocalPackgeVersion($localFile, $tempPath, $dir)
    {
        $version_file = "{$dir}/config/version.php";
        $files = array($version_file); //直接压需要的一部分
        $zip = new \ZipArchive;
        $res = $zip->open($localFile);
        if ($res === TRUE) {
            $zip->extractTo($tempPath, $files);
            $zip->close();
            $miniapp_config = "{$tempPath}/{$files[0]}";
            $config = [];
            if (is_file($miniapp_config)) {
                $config = include $miniapp_config;
            }
            @unlink($miniapp_config);
            return $config;
        } else {
            throw new MiniappException('本地包未找到应用配置');
        }
    }


    public function versionCheck($remote_version, $dir)
    {
        //得到当前应用配置
        $local_version = MiniappService::getVersion($dir);//应用本地版本
        $this->version = $local_version['version'];
        if (empty($local_version)) throw new MiniappException('未找到应用配置');
        //比较版本差异
        if ($this->compareVersion($this->version, $remote_version['version']) >= 0) throw new MiniappException('更新包版本低于当前应用版本，无需更新。');
        //返回
        return $remote_version;

    }


}